Skip to content

Conversation

@jirwin
Copy link
Contributor

@jirwin jirwin commented Oct 8, 2025

In postgres, you are unable to drop a role(account) if that role owns any objects. A role being granted privileges count as objects owned by the role. This means if you try and drop a role with any grants, we'll fail.

Postgres offers the DROP OWNED BY <role> command to drop any objects owned by the role. It also offers REASSIGNED OWNED BY <role> to <new_role>. We don't have the required context to do REASSIGN, and DROP comes with a risk that it is all objects, including things like tables/views/functions etc. Postgres will block the command from running if you try and drop an object that something (not owned by the role) depends on. Additionally, the user must be removed from any other role assignments before it can be dropped.

This felt like too much risk, so instead I've added a few functions that search for any grants to the role, as well as any role memberships and removes these first, and then removes the role. It also checks to see if the user owns any "database objects" like tables. If it does, we will fail with a specific error saying that these objects should be reassigned first.

Summary by CodeRabbit

  • New Features

    • Safer, multi-step role deletion workflow with ownership checks, automatic revocation of grants across schemas, functions, types and databases, and removal from parent role memberships.
    • Validation to prevent deleting empty/unnamed roles and clearer success messages.
  • Bug Fixes

    • Prevents deleting roles that still own database objects to avoid inconsistent states.
    • Improved error handling and logging for more reliable diagnostics.

@coderabbitai
Copy link

coderabbitai bot commented Oct 8, 2025

Walkthrough

Adds a multi-step safe deletion workflow for PostgreSQL roles: introduces ownership checks, grant revocation, membership removal, and helper methods; updates DeleteRole to delegate to SafeDeleteRole which orchestrates validation, checks, revocations, removals, DROP ROLE, and logging.

Changes

Cohort / File(s) Summary
Postgres role management
pkg/postgres/roles.go
Added RoleOwnsObjects(ctx, roleName) (bool, error), RevokeAllGrantsFromRole(ctx, roleName) error, RemoveRoleFromAllRoles(ctx, roleName) error, and SafeDeleteRole(ctx, roleName) error. Updated DeleteRole to delegate to SafeDeleteRole. Implemented ownership checks, grant revocation across schemas/functions/types/database, membership removal, DROP ROLE, detailed logging, and error handling for safe role deletion.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant Client
  participant Postgres as PostgreSQL

  Caller->>Client: DeleteRole(roleName)
  Client->>Client: delegate to SafeDeleteRole(roleName)
  Client->>Client: validate non-empty roleName
  Client->>Postgres: RoleOwnsObjects(roleName)?
  Postgres-->>Client: ownsObjects (bool) / error
  alt ownsObjects == true
    Client-->>Caller: error (role owns objects)
  else
    Client->>Postgres: RevokeAllGrantsFromRole(roleName)
    Postgres-->>Client: ok / error
    Client->>Postgres: RemoveRoleFromAllRoles(roleName)
    Postgres-->>Client: ok / error
    Client->>Postgres: DROP ROLE roleName
    Postgres-->>Client: ok / error
    Client-->>Caller: success
  end
  note over Client,Postgres: Each step logs context and surfaces errors
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I thump my paws — a tidy job to do,
Revoke the grants and unfriend the crew.
If objects cling, we pause the final leap,
Else roles fall gently, logs hum as I peep.
Tiny hops, big care — the database dreams anew 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title accurately summarizes the main change of adding pre-deletion checks and removal of grants and role memberships before deleting a role, and it is concise and clear for teammates to understand the primary update.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jirwin/improved-drop-role

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 104813d and 7c016d2.

⛔ Files ignored due to path filters (1)
  • .github/workflows/ci.yaml is excluded by none and included by none
📒 Files selected for processing (1)
  • pkg/postgres/roles.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/postgres/roles.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: go-test (1.23.x, ubuntu-latest)

Comment @coderabbitai help to get the list of available commands and usage tips.

@jirwin jirwin force-pushed the jirwin/improved-drop-role branch from 6cb9ad6 to 5ec8362 Compare October 8, 2025 21:54
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
pkg/postgres/roles.go (3)

164-188: Optimize repeated subquery for role OID lookup.

The subquery (SELECT oid FROM pg_roles WHERE rolname = $1) is executed seven times within the UNION ALL, which is inefficient. Consider using a CTE to fetch the OID once and reference it in all subsequent checks.

Apply this diff to optimize the query:

 	query := `
-		SELECT EXISTS(
-			SELECT 1 FROM (
-				-- Check for owned schemas
-				SELECT 1 FROM pg_namespace WHERE nspowner = (SELECT oid FROM pg_roles WHERE rolname = $1)
-				UNION ALL
-				-- Check for owned tables
-				SELECT 1 FROM pg_class WHERE relowner = (SELECT oid FROM pg_roles WHERE rolname = $1)
-				UNION ALL
-				-- Check for owned functions
-				SELECT 1 FROM pg_proc WHERE proowner = (SELECT oid FROM pg_roles WHERE rolname = $1)
-				UNION ALL
-				-- Check for owned sequences
-				SELECT 1 FROM pg_class WHERE relowner = (SELECT oid FROM pg_roles WHERE rolname = $1) AND relkind = 'S'
-				UNION ALL
-				-- Check for owned views
-				SELECT 1 FROM pg_class WHERE relowner = (SELECT oid FROM pg_roles WHERE rolname = $1) AND relkind = 'v'
-				UNION ALL
-				-- Check for owned types
-				SELECT 1 FROM pg_type WHERE typowner = (SELECT oid FROM pg_roles WHERE rolname = $1)
-				UNION ALL
-				-- Check for owned databases
-				SELECT 1 FROM pg_database WHERE datdba = (SELECT oid FROM pg_roles WHERE rolname = $1)
-			) owned_objects
-		)`
+		WITH role_oid AS (
+			SELECT oid FROM pg_roles WHERE rolname = $1
+		)
+		SELECT EXISTS(
+			SELECT 1 FROM (
+				-- Check for owned schemas
+				SELECT 1 FROM pg_namespace WHERE nspowner = (SELECT oid FROM role_oid)
+				UNION ALL
+				-- Check for owned tables, sequences, views, etc.
+				SELECT 1 FROM pg_class WHERE relowner = (SELECT oid FROM role_oid)
+				UNION ALL
+				-- Check for owned functions
+				SELECT 1 FROM pg_proc WHERE proowner = (SELECT oid FROM role_oid)
+				UNION ALL
+				-- Check for owned types
+				SELECT 1 FROM pg_type WHERE typowner = (SELECT oid FROM role_oid)
+				UNION ALL
+				-- Check for owned databases
+				SELECT 1 FROM pg_database WHERE datdba = (SELECT oid FROM role_oid)
+			) owned_objects
+		)`

329-370: Consider wrapping operations in a transaction.

The safe deletion process involves multiple database operations (checking ownership, revoking grants, removing memberships, dropping the role). If any step fails midway, the database is left in an inconsistent state. While the operations are idempotent and can be retried, wrapping them in a transaction would ensure atomicity.

Note: PostgreSQL DDL statements like REVOKE and DROP ROLE are transactional, so a transaction would provide rollback capability if any step fails.

Example structure:

func (c *Client) SafeDeleteRole(ctx context.Context, roleName string) error {
	l := ctxzap.Extract(ctx)

	if roleName == "" {
		return errors.New("role name cannot be empty")
	}

	// Start transaction
	tx, err := c.db.Begin(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback(ctx)

	// Perform operations using tx instead of c.db
	// ... ownership check, revoke grants, remove memberships, drop role ...

	// Commit transaction
	return tx.Commit(ctx)
}

However, implementing this requires refactoring RevokeAllGrantsFromRole and RemoveRoleFromAllRoles to accept a transaction or executor interface.


200-276: Consider revoking DEFAULT PRIVILEGES.

The current implementation revokes direct grants on existing objects but does not revoke DEFAULT PRIVILEGES. If the role has DEFAULT PRIVILEGES grants (which apply to future objects), those should also be revoked before deletion.

Add this after line 273:

 	}
 
+	// Revoke default privileges
+	for _, schema := range schemas {
+		sanitizedSchema := pgx.Identifier{schema}.Sanitize()
+		revokeDefaultQuery := fmt.Sprintf("ALTER DEFAULT PRIVILEGES FOR ROLE %s IN SCHEMA %s REVOKE ALL ON TABLES FROM %s", sanitizedRoleName, sanitizedSchema, sanitizedRoleName)
+		l.Debug("revoking default table privileges", zap.String("query", revokeDefaultQuery))
+		if _, err := c.db.Exec(ctx, revokeDefaultQuery); err != nil {
+			l.Error("error revoking default table privileges", zap.String("schema", schema), zap.Error(err))
+			return err
+		}
+		// Repeat for SEQUENCES, FUNCTIONS, TYPES as needed
+	}
+
 	revokeDbQuery := fmt.Sprintf("REVOKE ALL ON DATABASE %s FROM %s", pgx.Identifier{c.DatabaseName()}.Sanitize(), sanitizedRoleName)

Note: This requires checking which roles the target role granted DEFAULT PRIVILEGES for, which may require querying pg_default_acl.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 548a3a8 and 5ec8362.

📒 Files selected for processing (1)
  • pkg/postgres/roles.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
pkg/postgres/roles.go (1)
pkg/postgres/client.go (2)
  • Client (98-102)
  • New (121-148)
🪛 GitHub Check: go-lint
pkg/postgres/roles.go

[failure] 278-278:
Comment should end in a period (godot)

🔇 Additional comments (3)
pkg/postgres/roles.go (3)

279-327: LGTM!

The logic correctly identifies parent roles and removes the target role from each membership. Error handling appropriately returns errors immediately.


343-345: Good error message for ownership check.

The error message clearly instructs users to transfer ownership or drop objects before deleting the role, which is helpful for troubleshooting.


372-374: LGTM!

The delegation to SafeDeleteRole maintains backward compatibility while implementing the safer deletion logic.

Comment on lines 240 to 273
if _, err := c.db.Exec(ctx, revokeTablesQuery); err != nil {
l.Warn("error revoking table grants", zap.String("schema", schema), zap.Error(err))
}

revokeSequencesQuery := fmt.Sprintf("REVOKE ALL ON ALL SEQUENCES IN SCHEMA %s FROM %s", sanitizedSchema, sanitizedRoleName)
l.Debug("revoking sequence grants", zap.String("query", revokeSequencesQuery))
if _, err := c.db.Exec(ctx, revokeSequencesQuery); err != nil {
l.Warn("error revoking sequence grants", zap.String("schema", schema), zap.Error(err))
}

revokeFunctionsQuery := fmt.Sprintf("REVOKE ALL ON ALL FUNCTIONS IN SCHEMA %s FROM %s", sanitizedSchema, sanitizedRoleName)
l.Debug("revoking function grants", zap.String("query", revokeFunctionsQuery))
if _, err := c.db.Exec(ctx, revokeFunctionsQuery); err != nil {
l.Warn("error revoking function grants", zap.String("schema", schema), zap.Error(err))
}

revokeTypesQuery := fmt.Sprintf("REVOKE ALL ON ALL TYPES IN SCHEMA %s FROM %s", sanitizedSchema, sanitizedRoleName)
l.Debug("revoking type grants", zap.String("query", revokeTypesQuery))
if _, err := c.db.Exec(ctx, revokeTypesQuery); err != nil {
l.Warn("error revoking type grants", zap.String("schema", schema), zap.Error(err))
}

revokeSchemaQuery := fmt.Sprintf("REVOKE ALL ON SCHEMA %s FROM %s", sanitizedSchema, sanitizedRoleName)
l.Debug("revoking schema grants", zap.String("query", revokeSchemaQuery))
if _, err := c.db.Exec(ctx, revokeSchemaQuery); err != nil {
l.Warn("error revoking schema grants", zap.String("schema", schema), zap.Error(err))
}
}

revokeDbQuery := fmt.Sprintf("REVOKE ALL ON DATABASE %s FROM %s", pgx.Identifier{c.DatabaseName()}.Sanitize(), sanitizedRoleName)
l.Debug("revoking database grants", zap.String("query", revokeDbQuery))
if _, err := c.db.Exec(ctx, revokeDbQuery); err != nil {
l.Warn("error revoking database grants", zap.Error(err))
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Errors during grant revocation should not be silenced.

Grant revocation errors are logged as warnings but not returned (lines 241, 247, 253, 259, 265, 272). If revocation fails, SafeDeleteRole proceeds to drop the role, which may then fail with unclear error messages. Additionally, partial revocation leaves the database in an inconsistent state.

Consider either:

  1. Returning the first revocation error immediately to halt the process.
  2. Collecting all errors and returning them as a combined error after attempting all revocations.

Apply this diff for approach 1 (fail fast):

 		revokeTablesQuery := fmt.Sprintf("REVOKE ALL ON ALL TABLES IN SCHEMA %s FROM %s", sanitizedSchema, sanitizedRoleName)
 		l.Debug("revoking table grants", zap.String("query", revokeTablesQuery))
 		if _, err := c.db.Exec(ctx, revokeTablesQuery); err != nil {
-			l.Warn("error revoking table grants", zap.String("schema", schema), zap.Error(err))
+			l.Error("error revoking table grants", zap.String("schema", schema), zap.Error(err))
+			return err
 		}
 
 		revokeSequencesQuery := fmt.Sprintf("REVOKE ALL ON ALL SEQUENCES IN SCHEMA %s FROM %s", sanitizedSchema, sanitizedRoleName)
 		l.Debug("revoking sequence grants", zap.String("query", revokeSequencesQuery))
 		if _, err := c.db.Exec(ctx, revokeSequencesQuery); err != nil {
-			l.Warn("error revoking sequence grants", zap.String("schema", schema), zap.Error(err))
+			l.Error("error revoking sequence grants", zap.String("schema", schema), zap.Error(err))
+			return err
 		}
 
 		revokeFunctionsQuery := fmt.Sprintf("REVOKE ALL ON ALL FUNCTIONS IN SCHEMA %s FROM %s", sanitizedSchema, sanitizedRoleName)
 		l.Debug("revoking function grants", zap.String("query", revokeFunctionsQuery))
 		if _, err := c.db.Exec(ctx, revokeFunctionsQuery); err != nil {
-			l.Warn("error revoking function grants", zap.String("schema", schema), zap.Error(err))
+			l.Error("error revoking function grants", zap.String("schema", schema), zap.Error(err))
+			return err
 		}
 
 		revokeTypesQuery := fmt.Sprintf("REVOKE ALL ON ALL TYPES IN SCHEMA %s FROM %s", sanitizedSchema, sanitizedRoleName)
 		l.Debug("revoking type grants", zap.String("query", revokeTypesQuery))
 		if _, err := c.db.Exec(ctx, revokeTypesQuery); err != nil {
-			l.Warn("error revoking type grants", zap.String("schema", schema), zap.Error(err))
+			l.Error("error revoking type grants", zap.String("schema", schema), zap.Error(err))
+			return err
 		}
 
 		revokeSchemaQuery := fmt.Sprintf("REVOKE ALL ON SCHEMA %s FROM %s", sanitizedSchema, sanitizedRoleName)
 		l.Debug("revoking schema grants", zap.String("query", revokeSchemaQuery))
 		if _, err := c.db.Exec(ctx, revokeSchemaQuery); err != nil {
-			l.Warn("error revoking schema grants", zap.String("schema", schema), zap.Error(err))
+			l.Error("error revoking schema grants", zap.String("schema", schema), zap.Error(err))
+			return err
 		}
 	}
 
 	revokeDbQuery := fmt.Sprintf("REVOKE ALL ON DATABASE %s FROM %s", pgx.Identifier{c.DatabaseName()}.Sanitize(), sanitizedRoleName)
 	l.Debug("revoking database grants", zap.String("query", revokeDbQuery))
 	if _, err := c.db.Exec(ctx, revokeDbQuery); err != nil {
-		l.Warn("error revoking database grants", zap.Error(err))
+		l.Error("error revoking database grants", zap.Error(err))
+		return err
 	}

Copy link

@btipling btipling left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't test the failure case where we intentionally fail, but I did test that account creation and delete worked:

❯ ./dist/darwin_arm64/baton-postgresql --dsn "postgresql://localhost:5432/btipling?sslmode=disable" --create-account-login "testing1234_connector_role"

❯ ./dist/darwin_arm64/baton-postgresql --dsn "postgresql://localhost:5432/btipling?sslmode=disable"

❯ baton principals | grep testing1234
role:16394 | testing1234_connector_role  | Role   


❯  ./dist/darwin_arm64/baton-postgresql --dsn "postgresql://localhost:5432/btipling?sslmode=disable"  --grant-entitlement="entitlement:database:16384:connect" --grant-principal="role:16394" --grant-principal-type="role" 


❯ psql -d postgres -c "SELECT rolname FROM pg_roles WHERE rolname LIKE 'testing1234%';"
          rolname
----------------------------
 testing1234_connector_role
(1 row)

❯ ./dist/darwin_arm64/baton-postgresql --dsn "postgresql://localhost:5432/btipling?sslmode=disable"

❯ baton grants | grep 16394
grant:entitlement:schema:db16384:2200:usage:role:16394     | Schema        | btipling - public    | USAGE               | testing1234_connector_role
grant:entitlement:database:16384:temporary:role:16394      | Database      | btipling             | TEMPORARY           | testing1234_connector_role
grant:entitlement:database:16384:connect:role:16394        | Database      | btipling             | CONNECT             | testing1234_connector_role

./dist/darwin_arm64/baton-postgresql --dsn "postgresql://localhost:5432/btipling?sslmode=disable" --delete-resource="role:16394"  --delete-resource-type="role" 

./dist/darwin_arm64/baton-postgresql --dsn "postgresql://localhost:5432/btipling?sslmode=disable"


❯ baton grants | grep 16394

❯ baton principals | grep testing1234

❯ psql -d postgres -c "SELECT rolname FROM pg_roles WHERE rolname LIKE 'testing1234%';"
 rolname
---------
(0 rows)



Copy link
Contributor

@ggreer ggreer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments but no blockers to merging. Also adding a delete role CI test would be nice.

l.Debug("removing role from parent role", zap.String("query", revokeQuery))
if _, err := c.db.Exec(ctx, revokeQuery); err != nil {
l.Error("error removing role from parent role", zap.String("parent_role", parentRole), zap.Error(err))
return err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: I think this error would get bubbled up to the user. Do we want to use fmt.Errorf here to make it clearer what the failure is?

(The same goes for other places where we just return err.)

revokeDbQuery := fmt.Sprintf("REVOKE ALL ON DATABASE %s FROM %s", pgx.Identifier{c.DatabaseName()}.Sanitize(), sanitizedRoleName)
l.Debug("revoking database grants", zap.String("query", revokeDbQuery))
if _, err := c.db.Exec(ctx, revokeDbQuery); err != nil {
l.Warn("error revoking database grants", zap.Error(err))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to return these errors? Would it be better to join them and return any errors in this function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the play is join them, and return them if the final role deletion fails. Will work on that.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
pkg/postgres/roles.go (1)

174-183: Remove redundant sequence and view checks.

Lines 180 and 183 duplicate the check on Line 174, which already covers all pg_class objects (tables, sequences, views, etc.) via the relowner filter. The separate UNION ALL clauses for sequences (relkind = 'S') and views (relkind = 'v') are unnecessary.

Based on learnings

Apply this diff to remove the redundant checks:

 		SELECT EXISTS(
 			SELECT 1 FROM (
 				-- Check for owned schemas
 				SELECT 1 FROM pg_namespace WHERE nspowner = (SELECT oid FROM pg_roles WHERE rolname = $1)
 				UNION ALL
 				-- Check for owned tables
 				SELECT 1 FROM pg_class WHERE relowner = (SELECT oid FROM pg_roles WHERE rolname = $1)
 				UNION ALL
 				-- Check for owned functions
 				SELECT 1 FROM pg_proc WHERE proowner = (SELECT oid FROM pg_roles WHERE rolname = $1)
 				UNION ALL
-				-- Check for owned sequences
-				SELECT 1 FROM pg_class WHERE relowner = (SELECT oid FROM pg_roles WHERE rolname = $1) AND relkind = 'S'
-				UNION ALL
-				-- Check for owned views
-				SELECT 1 FROM pg_class WHERE relowner = (SELECT oid FROM pg_roles WHERE rolname = $1) AND relkind = 'v'
-				UNION ALL
 				-- Check for owned types
 				SELECT 1 FROM pg_type WHERE typowner = (SELECT oid FROM pg_roles WHERE rolname = $1)
 				UNION ALL
 				-- Check for owned databases
 				SELECT 1 FROM pg_database WHERE datdba = (SELECT oid FROM pg_roles WHERE rolname = $1)
 			) owned_objects
 		)`
🧹 Nitpick comments (1)
pkg/postgres/roles.go (1)

395-397: Clarify the error handling intent.

The errors.Is check on Line 395 prevents early return when grantsRevokeError is wrapped with errRevokeGrantsFromRole. Since RevokeAllGrantsFromRole always wraps errors with this sentinel (Line 311), this condition will never be true. If the intent is to always continue despite grant revocation failures and report them at the end, consider simplifying or adding a comment to clarify this behavior.

Consider simplifying to:

 	l.Debug("revoking all grants from role", zap.String("role", roleName))
 	grantsRevokeError := c.RevokeAllGrantsFromRole(ctx, roleName)
 	if grantsRevokeError != nil {
 		l.Error("error revoking grants from role", zap.Error(grantsRevokeError))
-		if !errors.Is(grantsRevokeError, errRevokeGrantsFromRole) {
-			return fmt.Errorf("error revoking existing grants from role: %w", grantsRevokeError)
-		}
+		// Continue to attempt role deletion; errors will be joined at the end if DROP ROLE fails
 	}

Or add a comment explaining the intent if you prefer to keep the check.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2659003 and 104813d.

⛔ Files ignored due to path filters (1)
  • .github/workflows/ci.yaml is excluded by none and included by none
📒 Files selected for processing (1)
  • pkg/postgres/roles.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
pkg/postgres/roles.go (1)
pkg/postgres/client.go (2)
  • New (121-148)
  • Client (98-102)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: go-test (1.23.x, ubuntu-latest)
🔇 Additional comments (5)
pkg/postgres/roles.go (5)

17-18: LGTM!

The sentinel errors are well-defined and will help identify the source of errors when wrapped with errors.Join.


238-314: Error collection approach looks good.

The method now collects all revocation errors using errors.Join and returns them wrapped with a sentinel error. This is an improvement over silencing errors, as it attempts to revoke as many grants as possible while still reporting all failures.

Based on learnings


317-371: LGTM!

The error handling pattern is consistent with RevokeAllGrantsFromRole, collecting all failures while attempting to remove the role from all parent roles.


409-420: Error aggregation strategy is sound.

The approach of attempting all cleanup steps and then joining all errors (Line 415) provides comprehensive failure reporting. This ensures that partial failures are visible while still attempting maximum cleanup.


423-425: LGTM!

Clean delegation to SafeDeleteRole maintains the public API while adding the safety checks.

@jirwin jirwin merged commit 5981e8e into main Oct 14, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants